﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Xml;
using NCMIS.ObjectModel;
using NCMIS.ObjectModel.MetaData;
using NCMIS.ObjectModel.Rest;
using NCMIS.Produce;

namespace NCMIS.ServiceModel.Syndication
{
    /// <summary>
    /// Represents a CMIS Atom feed item, extended with Atom Extensions for CMIS.
    /// </summary>
    public class CmisSyndicationFeed : SyndicationFeed
    {
        #region Properties

        /// <summary>
        /// Gets or sets the RepositoryId associated with this feed.
        /// </summary>
        public string RepositoryId
        {
            get;
            set;
        }

        #endregion

        #region SyndicationLinks

        // The SyndicationLinks will have defaulted to null. If user enters a URI they will also be assigned their
        // appropriate RelationshipType and MediaType. Only link != null should be serialized to XML.
        private SyndicationLink _downLink;
        private SyndicationLink _firstLink;
        private SyndicationLink _lastLink;
        private SyndicationLink _nextLink;
        private SyndicationLink _previousLink;
        private SyndicationLink _selfLink;
        private SyndicationLink _serviceLink;
        private SyndicationLink _upLink;
        private SyndicationLink _viaLink;

        #region Setters

        /// <summary>
        /// Sets the Down Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri DownLinkUri
        {
            set
            {
                if (value == null)
                {
                    _downLink = null;
                }
                else
                {
                    _downLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Down,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }

        /// <summary>
        /// Sets the First Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri FirstLinkUri
        {
            set
            {
                if (value == null)
                {
                    _firstLink = null;
                }
                else
                {
                    _firstLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.First,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }
        
        /// <summary>
        /// Sets the Last Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri LastLinkUri
        {
            set
            {
                if (value == null)
                {
                    _lastLink = null;
                }
                else
                {
                    _lastLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Last,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }

        /// <summary>
        /// Sets the Next Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri NextLinkUri
        {
            set
            {
                if (value == null)
                {
                    _nextLink = null;
                }
                else
                {
                    _nextLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Next,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }

        /// <summary>
        /// Sets the Previous Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri PreviousLinkUri
        {
            set
            {
                if (value == null)
                {
                    _previousLink = null;
                }
                else
                {
                    _previousLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Previous,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }

        /// <summary>
        /// Sets the Self Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri SelfLinkUri
        {
            set
            {
                if (value == null)
                {
                    _selfLink = null;
                }
                else
                {
                    _selfLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Self,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }

        /// <summary>
        /// Sets the Service Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri ServiceLinkUri
        {
            set
            {
                if (value == null)
                {
                    _serviceLink = null;
                }
                else
                {
                    _serviceLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Service,
                        MediaType = CmisContentType.Service,
                    };
                }
            }
        }

        /// <summary>
        /// Sets the Up Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri UpLinkUri
        {
            set
            {
                if (value == null)
                {
                    _upLink = null;
                }
                else
                {
                    _upLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Up,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }

        /// <summary>
        /// Sets the Via Link URI, and assigns its RelationshipType and MediaType.
        /// </summary>
        public Uri ViaLinkUri
        {
            set
            {
                if (value == null)
                {
                    _viaLink = null;
                }
                else
                {
                    _viaLink = new SyndicationLink()
                    {
                        Uri = value,
                        RelationshipType = CmisLinkRel.Via,
                        MediaType = CmisContentType.Feed,
                    };
                }
            }
        }

        #endregion

        #region Getters

        /// <summary>
        /// Gets the Down link.
        /// </summary>
        public SyndicationLink DownLink
        {
            get
            {
                return _downLink;
            }
        }

        /// <summary>
        /// Gets the First link, used for paging.
        /// </summary>
        public SyndicationLink FirstLink
        {
            get
            {
                return _firstLink;
            }
        }

        /// <summary>
        /// Gets the Last link, used for paging.
        /// </summary>
        public SyndicationLink LastLink
        {
            get
            {
                return _lastLink;
            }
        }

        /// <summary>
        /// Gets the Next link, used for paging.
        /// </summary>
        public SyndicationLink NextLink
        {
            get
            {
                return _nextLink;
            }
        }

        /// <summary>
        /// Gets the Previous link, used for paging.
        /// </summary>
        public SyndicationLink PreviousLink
        {
            get
            {
                return _previousLink;
            }
        }

        /// <summary>
        /// Gets the Self link, pointing to this feed.
        /// </summary>
        public SyndicationLink SelfLink
        {
            get
            {
                return _selfLink;
            }
        }

        /// <summary>
        /// Gets the Service Link, pointing to the service document.
        /// </summary>
        public SyndicationLink ServiceLink
        {
            get
            {
                return _serviceLink;
            }
        }

        /// <summary>
        /// Gets the Up Link.
        /// </summary>
        public SyndicationLink UpLink
        {
            get
            {
                return _upLink;
            }
        }

        /// <summary>
        /// Gets the Via Link.
        /// </summary>
        public SyndicationLink ViaLink
        {
            get
            {
                return _viaLink;
            }
        }

        #endregion

        #endregion

        #region Constructors


        public CmisSyndicationFeed()
        {
            // XML Namespaces
            this.AttributeExtensions.Add(new XmlQualifiedName("cmis", "http://www.w3.org/2000/xmlns/"), CmisNs.Cmis);
            this.AttributeExtensions.Add(new XmlQualifiedName("cmism", "http://www.w3.org/2000/xmlns/"), CmisNs.Cmism);
            this.AttributeExtensions.Add(new XmlQualifiedName("atom", "http://www.w3.org/2000/xmlns/"), CmisNs.Atom);
            this.AttributeExtensions.Add(new XmlQualifiedName("app", "http://www.w3.org/2000/xmlns/"), CmisNs.App);
            this.AttributeExtensions.Add(new XmlQualifiedName("cmisra", "http://www.w3.org/2000/xmlns/"), CmisNs.Cmisra);
        }

        #endregion

        #region Atom Extensions

        /// <summary>
        /// Adds cmisra:numItems element extension. Must be called after all entries have been added to feed.
        /// </summary>
        public void AddNumItems()
        {
            this.ElementExtensions.Add("numItems", CmisNs.Cmisra, this.Items.Count<SyndicationItem>());
        }

        /// <summary>
        /// Adds cmisra:hasMoreItems element extension.
        /// </summary>
        /// <param name="value"></param>
        public void AddHasMoreItems(bool value)
        {
            this.ElementExtensions.Add("hasMoreItems", CmisNs.Cmisra, value);
        }

        #endregion

        #region Helper Methods

        private void AddLinks()
        {
            if (_downLink != null) this.Links.Add(_downLink);
            if (_firstLink != null) this.Links.Add(_firstLink);
            if (_lastLink != null) this.Links.Add(_lastLink);
            if (_nextLink != null) this.Links.Add(_nextLink);
            if (_previousLink != null) this.Links.Add(_previousLink);
            if (_selfLink != null) this.Links.Add(_selfLink);
            if (_serviceLink != null) this.Links.Add(_serviceLink);
            if (_viaLink != null) this.Links.Add(_viaLink);
            if (_upLink != null) this.Links.Add(_upLink);
        }

        private SyndicationItem AddItem(PathedCmisObject pathedCmisObject)
        {
            List<SyndicationItem> entries = new List<SyndicationItem>();
            CmisSyndicationItem entry = CmisSyndicationItem.CreateEntry(this.RepositoryId, pathedCmisObject.Object);

            AddLinks();

            if (pathedCmisObject.RelativePathSegment != null)
            {
                entry.AddPathSegment(pathedCmisObject.RelativePathSegment);
            }

            entries.Add(entry);
            if (this.Items != null)
            {
                entries.AddRange(this.Items);
            }
            this.Items = entries;

            return entry;
        }

        #endregion

        #region Add Methods (CmisObject)

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="source">The <see cref="NCMIS.ObjectModel.CmisObjectList"/> instance to be added.</param>
        public void AddEntries(CmisObjectList source)
        {
            AddEntries(source.Objects);
            AddHasMoreItems(source.HasMoreItems);
            AddNumItems();
        }

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="cmisObjects">The <see cref="NCMIS.ObjectModel.CmisObjects"/> instance to be added.</param>
        public void AddEntries(CmisObjects cmisObjects)
        {
            AddEntries(cmisObjects.ToArray());
        }

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="cmisObjects">The <see cref="NCMIS.ObjectModel.CmisObject"/> collection instance to be added.</param>
        public void AddEntries(CmisObject[] cmisObjects)
        {
            List<SyndicationItem> entries = new List<SyndicationItem>();

            foreach (CmisObject cmisObject in cmisObjects)
            {
                CmisSyndicationItem entry = CmisSyndicationItem.CreateEntry(this.RepositoryId, cmisObject);
                entries.Add(entry);
            }

            AddLinks();

            this.Items = entries;
        }

        #endregion

        #region Add Methods (TypeContainer)

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="types">The <see cref="NCMIS.ObjectModel.MetaData.TypeContainer"/> collection instance to be added.</param>
        public void AddEntries(TypeContainer[] types)
        {
            List<SyndicationItem> entries = new List<SyndicationItem>();

            foreach (TypeContainer typeContainer in types)
            {
                CmisSyndicationItem typeEntry = CmisSyndicationItem.CreateEntry(this.RepositoryId, typeContainer.Type);

                // TODO: When adding the type definitions the DataContract is used instead of the ToXElement method. Change to ToXElement method?
                switch (typeContainer.Type.BaseId)
                {
                    case BaseObjectTypeId.CmisDocument:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypeDocumentDefinition);
                        break;
                    case BaseObjectTypeId.CmisFolder:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypeFolderDefinition);
                        break;
                    case BaseObjectTypeId.CmisPolicy:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypePolicyDefinition);
                        break;
                    case BaseObjectTypeId.CmisRelationship:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypeRelationshipDefinition);
                        break;
                }

                CmisChildren children = new CmisChildren()
                {
                    Author = new CmisAuthor() { Name = "Admin" },
                    Id = String.Format("urn:uuid:{0}-descendants", typeContainer.Type.Id),
                    Title = String.Format("{0} Descendants", typeContainer.Type.DisplayName),
                    Updated = new DateTimeOffset(DateTime.Now).ToString(),
                    Entry = new CmisEntry()
                    {
                        Type = typeContainer.Type.ToXElement()
                    }
                };

                entries.Add(typeEntry);

                typeEntry.ElementExtensions.Add("children", CmisNs.Cmisra, children);
            }

            AddLinks();

            this.Items = entries;
        }

        #endregion

        #region Add Methods (PathedCmisObject)

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="pathedCmisObjectContainers">The <see cref="NCMIS.ObjectModel.PathedCmisObjectContainer"/> collection instance to be added.</param>
        public void AddEntries(PathedCmisObjectContainer[] pathedCmisObjectContainers)
        {
            foreach (PathedCmisObjectContainer pathedCmisObjectContainer in pathedCmisObjectContainers)
            {
                AddEntries(pathedCmisObjectContainer);
            }
            AddNumItems();
        }

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="pathedCmisObjectContainer">The <see cref="NCMIS.ObjectModel.PathedCmisObjectContainer"/> instance to be added.</param>
        public void AddEntries(PathedCmisObjectContainer pathedCmisObjectContainer)
        {
            List<SyndicationItem> entries = new List<SyndicationItem>();
            PathedCmisObject pathedCmisObject = pathedCmisObjectContainer.ObjectInFolder;

            CmisSyndicationItem entry = CmisSyndicationItem.CreateEntry(this.RepositoryId, pathedCmisObject.Object);

            if (pathedCmisObject.RelativePathSegment != null)
            {
                entry.AddPathSegment(pathedCmisObject.RelativePathSegment);
            }

            entries.Add(entry);

            if (pathedCmisObjectContainer.Children != null)
            {
                this.AddChildren(pathedCmisObjectContainer.Children);
            }

            AddLinks();

            if (this.Items != null)
            {
                entries.AddRange(this.Items);
            }
            this.Items = entries;
        }

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="pathedCmisObjects">The <see cref="NCMIS.ObjectModel.PathedCmisObject"/> collection instance to be added.</param>
        public void AddEntries(PathedCmisObject[] pathedCmisObjects)
        {
            List<SyndicationItem> entries = new List<SyndicationItem>();

            foreach (PathedCmisObject pathedCmisObject in pathedCmisObjects)
            {
                CmisSyndicationItem objectEntry = CmisSyndicationItem.CreateEntry(this.RepositoryId, pathedCmisObject.Object);

                if (pathedCmisObject.RelativePathSegment != null)
                {
                    objectEntry.AddPathSegment(pathedCmisObject.RelativePathSegment);
                }

                entries.Add(objectEntry);
            }

            AddLinks();

            if (this.Items != null)
            {
                entries.AddRange(this.Items);
            }
            this.Items = entries;
        }

        /// <summary>
        /// Add CMIS objects as entries to the current feed.
        /// </summary>
        /// <param name="pathedCmisObjectList">The <see cref="NCMIS.ObjectModel.PathedCmisObjectList"/> instance to be added add.</param>
        public void AddEntries(PathedCmisObjectList pathedCmisObjectList)
        {
            AddEntries(pathedCmisObjectList.Objects);
            AddHasMoreItems(pathedCmisObjectList.HasMoreItems);
            AddNumItems();
        }

        #endregion

        public void AddChildren(PathedCmisObjectContainer[] children)
        {
            foreach (PathedCmisObjectContainer childContainer in children)
            {
                CmisChildren cmisChildren = new CmisChildren()
                {
                    Author = new CmisAuthor() { Name = "Admin" },
                    Id = String.Format("urn:uuid:{0}-descendants", childContainer.ObjectInFolder.Object.Id),
                    Title = String.Format("{0} Descendants", childContainer.ObjectInFolder.Object.Id),
                    Updated = new DateTimeOffset(DateTime.Now).ToString(),
                    Entry = new CmisEntry()
                    {
                        Object = childContainer.ObjectInFolder.Object.ToXElement()
                    }
                };

                this.ElementExtensions.Add("children", CmisNs.Cmisra, cmisChildren);
            }
        }

        /// <summary>
        /// Adds a <see cref="NCMIS.Objectmodel.TokenedCmisObjectList"/> to the current feed.
        /// </summary>
        /// <param name="tokenedCmisObjectList">The TokenedCmisObjectList to add.</param>
        public void AddEntries(TokenedCmisObjectList tokenedCmisObjectList)
        {
            List<SyndicationItem> entries = new List<SyndicationItem>();

            foreach (CmisObject cmisObject in tokenedCmisObjectList.Objects.Objects)
            {
                CmisSyndicationItem entry = CmisSyndicationItem.CreateEntry(this.RepositoryId, cmisObject);
                entries.Add(entry);
            }

            AddLinks();

            this.Items = entries;
        }

        /// <summary>
        /// Adds a cmism:typeDefinitionList element to this feed.
        /// </summary>
        /// <param name="typeDefinitionList">The type definition list to add to feed.</param>
        public void AddEntries(TypeDefinitionList typeDefinitionList)
        {
            List<SyndicationItem> entries = new List<SyndicationItem>();

            foreach (TypeDefinition typeDefinition in typeDefinitionList.Types)
            {
                if (typeDefinition == null)
                    continue;

                CmisSyndicationItem typeEntry = CmisSyndicationItem.CreateEntry(this.RepositoryId, typeDefinition);

                TypeContainer typeContainer = new TypeContainer()
                {
                    Type = typeDefinition,
                    Children = new TypeContainer[]
                    {
                        new TypeContainer()
                        {
                            Type = typeDefinition,
                            Children = null,
                        }
                    }
                };

                // TODO: When adding the type definitions the DataContract is used instead of the ToXElement method. Change to ToXElement method?
                switch (typeDefinition.BaseId)
                {
                    case BaseObjectTypeId.CmisDocument:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypeDocumentDefinition);
                        break;
                    case BaseObjectTypeId.CmisFolder:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypeFolderDefinition);
                        break;
                    case BaseObjectTypeId.CmisPolicy:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypePolicyDefinition);
                        break;
                    case BaseObjectTypeId.CmisRelationship:
                        typeEntry.ElementExtensions.Add("type", CmisNs.Cmisra, typeContainer.Type as TypeRelationshipDefinition);
                        break;
                }

                // cmisra:children (extension) (dataContractExtension object of type TypeContainer.Children : TypeContainer (serialized as children))
                //typeEntry.ElementExtensions.Add("children", CmisNs.Cmisra, typeContainer.Children);

                entries.Add(typeEntry);
            }

            AddLinks();

            this.Items = entries;
        }

        public static SyndicationLink GetLink(SyndicationFeed feed, string linkRel)
        {
            SyndicationLink result = null;
            foreach (SyndicationLink link in feed.Links.Where<SyndicationLink>(x => x.RelationshipType == linkRel))
            {
                result = link;
                break;
            }
            return result;
        }

        #region Factory Methods

        public static SyndicationFeed CreateFeed(Uri baseUri, string repositoryId, CmisObject cmisObject, PathedCmisObjectList pathedCmisObjectList, string feedType)
        {
            return CreateFeedInternal(baseUri, repositoryId, cmisObject, pathedCmisObjectList, feedType);
        }

        public static SyndicationFeed CreateFeed(Uri baseUri, string repositoryId, CmisObject cmisObject, PathedCmisObjectContainer[] pathedCmisObjectContainers, string feedType)
        {
            return CreateFeedInternal(baseUri, repositoryId, cmisObject, pathedCmisObjectContainers, feedType);
        }

        private static SyndicationFeed CreateFeedInternal(Uri baseUri, string repositoryId, CmisObject cmisObject, object objectContainer, string feedType)
        {
            string id = cmisObject.Properties.ObjectId.SingleValue;
            string name = cmisObject.Properties.Name.SingleValue;
            string createdBy = cmisObject.Properties.CreatedBy.SingleValue;

            string[] cmisCreatedBy = cmisObject.Properties.CreatedBy.Value;

            Uri serviceUri = new Uri(baseUri, "Service.svc/");

            CmisSyndicationFeed feed = new CmisSyndicationFeed()
            {
                BaseUri = serviceUri,

                // atom:title MUST be the CMIS property cmis:name 
                Title = new TextSyndicationContent(name),
                // atom:id SHOULD be derived from cmis:objectId. This id MUST be compliant with atom’s specification and be a valid URI. 
                Id = String.Format("urn:uuid:{0}", id),
                // atom:updated SHOULD be the latest time the folder or its contents was updated. If unknown by the underlying repository, it MUST be the current time.
                LastUpdatedTime = new DateTimeOffset(DateTime.Now),
                RepositoryId = repositoryId,
                // The atom:link with relation self MUST be generated to return the URI of the feed.
                //SelfLinkUri = new Uri(CmisRestAtom.ServiceBaseUri, String.Format("{0}/children/{1}{2}", repositoryId, id, NCMIS.ServiceModel.Ra.HttpRequest.GetQueryString())),
                ServiceLinkUri = new Uri(serviceUri, String.Format("{0}/servicedoc", repositoryId)),
                // When used on an Atom Feed document, this link relation MUST point to the atom entry representing the CMIS resource from whom this feed is derived.  
                ViaLinkUri = new Uri(serviceUri, String.Format("{0}/object/{1}", repositoryId, id)),
            };

            // The atom:link with relation self MUST be generated to return the URI of the feed.
            switch (feedType)
            {
                case "children":
                    feed.SelfLinkUri = new Uri(serviceUri, String.Format("{0}/children/{1}{2}", repositoryId, id, NCMIS.ServiceModel.Ra.HttpRequest.GetQueryString()));
                    break;
                case "descendants":
                    feed.SelfLinkUri = new Uri(serviceUri, String.Format("{0}/descendants/{1}", repositoryId, id));
                    break;
            }
            // TODO: A feed SHOULD contain the element app:collection, describing the appropriate media types supported for creation of new entries in the feed 

            // TODO: up-link, down-link
            //       first-, previous-, next-, last-link

            // atom:author/atom:name MUST be the CMIS property cmis:createdBy 
            feed.Authors.Add(new SyndicationPerson() { Name = createdBy });

            if (objectContainer != null)
            {
                // PathedCmisObjectList
                if (objectContainer.GetType().Equals(typeof(PathedCmisObjectList)))
                    feed.AddEntries(objectContainer as PathedCmisObjectList);

                // PathedCmisObjectContainer
                if (objectContainer.GetType().Equals(typeof(PathedCmisObjectContainer[])))
                    feed.AddEntries(objectContainer as PathedCmisObjectContainer[]);
            }

            feed.AddNumItems();

            return feed;
        }

        #endregion
    }
}
